package com.zqweb.gulimall.authserver.web;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.zqweb.common.constant.AuthServerConstant;
import com.zqweb.common.exception.ExpCodeAndMsgEnum;
import com.zqweb.common.utils.HttpUtils;
import com.zqweb.common.utils.R;
import com.zqweb.common.vo.GiteeAccessToken;
import com.zqweb.common.vo.MemberUserInfoVo;
import com.zqweb.gulimall.authserver.vo.GiteeAuthVo;
import com.zqweb.common.vo.GiteeSocialUserInfo;
import com.zqweb.common.vo.GiteeUserInfo;
import com.zqweb.gulimall.authserver.feign.MemberFeignService;
import com.zqweb.gulimall.authserver.feign.ThirdPartyFeignService;
import com.zqweb.gulimall.authserver.vo.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Controller
@Slf4j
public class LoginController {
    @Autowired
    private ThirdPartyFeignService thirdPartyFeignService;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private MemberFeignService memberFeignService;
    @Autowired
    private GiteeAuthVo giteeAuthVo;

    // 接入第三方社交登陆平台，在对应平台登陆认证授权后的回调，注意：是成功登陆后的回调
    @RequestMapping("/login/oauth2/back")
    public String loginBack(@RequestParam("code") String code, RedirectAttributes redirectAttributes,HttpSession httpSession) throws Exception {
        System.out.println("用户给Gitee平台授权登陆，得到Gitee传回的授权码为：" + code);
        if (StringUtils.isEmpty(code)) {
            Map<String,String> resMap = new HashMap<>();
            resMap.put("msg","授权失败，请重新授权！");
            redirectAttributes.addFlashAttribute("errs",resMap);
            return "redirect:http://auth.gulimall.com/login.html";
        } else {
            // 获取到Gitee服务器返回的授权码后，构建获取access_token的请求参数对象
            Map<String, String> reqBodys = new HashMap<>();
            reqBodys.put("grant_type", giteeAuthVo.getGrantType());
            reqBodys.put("code", code);
            reqBodys.put("client_id", giteeAuthVo.getClientId());
            reqBodys.put("redirect_uri", giteeAuthVo.getRedirectUri());
            reqBodys.put("client_secret", giteeAuthVo.getClientSecret());
            for (Map.Entry<String, String> entry : reqBodys.entrySet()) {
                System.out.println("reqBodys的key：" + entry.getKey() + "，value：" + entry.getValue());
            }
            // 通过HttpUtils工具类，发起doPost方法，传入请求对象，访问获取access_token的接口
            HttpResponse response = HttpUtils.doPost("https://gitee.com", "/oauth/token", "post", new HashMap<>(), new HashMap<>(), reqBodys);
            // 处理访问该接口后的响应结果
            if (response != null && response.getStatusLine().getStatusCode() == 200) {
                // 先获取响应结果中封装的实体信息，转为json的String串，然后解析成1个特定的token实体，从中获取到token信息
                String giteeAccessTokenJson = EntityUtils.toString(response.getEntity());
                // 保留该条测试打印语句，可以打印出第三方对接授权后的access_token接口访问情况（包括异常信息）
                System.out.println("获取到的tokenJSONString为：" + giteeAccessTokenJson);
                GiteeAccessToken accessToken = JSON.parseObject(giteeAccessTokenJson, GiteeAccessToken.class);
                String accessTokenString = accessToken.getAccess_token();
//                System.out.println("根据授权码访问获取到的access_token的token字符串为：" + accessTokenString);
                // 根据该token信息，构建获取登陆用户的Gitee相关信息（不包含私密的如密码等信息）的请求对象
                Map<String, String> reqQuerys = new HashMap<>();
                reqQuerys.put("access_token", accessTokenString);
                // 通过HttpUtils工具类，发起doGet方法，传入请求对象，访问获取用户相关信息
                HttpResponse userResponse = HttpUtils.doGet("https://gitee.com", "/api/v5/user", "get", new HashMap<>(), reqQuerys);
                HttpEntity userResponseEntity = userResponse.getEntity();
                if (userResponseEntity != null) {
                    String userJsonString = EntityUtils.toString(userResponseEntity);
                    GiteeUserInfo giteeUserInfo = JSON.parseObject(userJsonString, GiteeUserInfo.class);
//                    System.out.println("根据accessToken获取到Gitee相关用户信息并封装后的实体类为：" + giteeUserInfo);
                    /*
                        TODO: 登录或者注册这个社交用户
当前用户如果是第一次进入网站，就自动注册（为当前社交用户生成一个会员信息账号，以后这个社交账号就对应指定的会员）
如果不是第一次登陆，则根据获取的访问令牌及过期时间，去数据库中更新该用户的访问令牌及过期时间信息
                         */
                    GiteeSocialUserInfo giteeSocialUserInfo = new GiteeSocialUserInfo();
                    BeanUtils.copyProperties(giteeUserInfo,giteeSocialUserInfo);
                    giteeSocialUserInfo.setAccess_token(accessToken.getAccess_token());
                    giteeSocialUserInfo.setExpires_in(accessToken.getExpires_in());
                    R r = memberFeignService.loginByThirdparty(giteeSocialUserInfo);
                    if(r != null && r.getCode() == 0){
                        // 通过RedirectAttributes类，在请求重定向跳转时，携带调用远程member服务处理后的memberEntity类信息
                        MemberUserInfoVo memberUserInfo = r.getData("memberUserInfo", new TypeReference<MemberUserInfoVo>() {
                        });
                        // 将登陆的用户信息，保存到Session会话中，并以Redis形式存储（注意：实体类需要实现序列化接口——序列化和反序列化）
                        httpSession.setAttribute(AuthServerConstant.LOGIN_USER_KEY,memberUserInfo);
                        /*
                         TODO:
                            1、不同服务的子域session共享问题！
                            2、采用默认JDK序列化存储的方式，需要修改为JSON序列化的方式存储到Redis中！
                          */
                        log.info("第三方Gitee登陆接口对接并成功登陆，用户信息为：\n{}",memberUserInfo);

                        // 对接Gitee登陆接口，登陆成功，并获取到相关用户信息，然后携带这些用户信息，返回到商城首页，并在前端做好渲染
                        return "redirect:http://gulimall.com";
                    }else{
                        Map<String,String> resMap = new HashMap<>();
                        resMap.put("msg","远程服务不稳定，请重新登陆！");
                        redirectAttributes.addFlashAttribute("errs",resMap);
                        return "redirect:http://auth.gulimall.com/login.html";
                    }

                }else{
                    System.out.println("根据accessToken获取用户信息失败！");
                    Map<String,String> resMap = new HashMap<>();
                    resMap.put("msg","远程服务不稳定，请重新登陆！");
                    redirectAttributes.addFlashAttribute("errs",resMap);
                    return "redirect:http://auth.gulimall.com/login.html";
                }
            } else {
                Map<String,String> resMap = new HashMap<>();
                resMap.put("msg","远程服务不稳定，请重新登陆！");
                redirectAttributes.addFlashAttribute("errs",resMap);
                // 获取响应结果失败，即access_token获取不到，则重新回到auth服务的login.html，进行再次登陆
                return "redirect:http://auth.gulimall.com/login.html";
            }
        }

    }


    /*
     获取短信验证码：其实就是在后台生成1个指定位数的随机数（验证码），并将前端传来的手机号以及
     该服务生成的随机验证码，调用远程的第三方服务接口（传过去这两个参数），由第三方来调用实际的第三方服务接口，
     将指定的验证码发送给指定的手机号对应的用户。但是，这样子存在2个问题：
     问题一：短信发送接口直接在前端暴露，很容易被恶意访问，造成短信接口压力过大，费用飙升
     问题二：短信验证码发送过1次后，在指定时间内都是有效的，下次利用原先未过期的验证码仍然可使用，通过验证（验证码的再次利用）

      */
    @GetMapping("/auth/sms/sendCode")
    @ResponseBody
    public R sendCode(@RequestParam("phone") String phone){
        System.out.println("手机号码为：" + phone);
    // 问题一：60s内重新刷新页面，页面js定时效果重新初始化归零，可重新发起获取验证码请求，重复调用第三方接口问题
    // 解决：将上一次获取验证码存入缓存的时间，与当前请求时间作比较，60s内，直接返回异常提示，不调用第三方接口，60s后，才继续调用
        String code = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
        if(!StringUtils.isEmpty(code)){
            Long storeTime = Long.parseLong(code.split("_")[1]);
            Long curTime = System.currentTimeMillis();
            // 如果小于60s，直接返回异常提示
            if((curTime - storeTime) < 60000){
                return R.error(ExpCodeAndMsgEnum.AUTH_CODE_GET_FAST_EXCEPTION.getCode(),ExpCodeAndMsgEnum.AUTH_CODE_GET_FAST_EXCEPTION.getMsg());
            }
        }

        // 生成1个6位的随机整数（100000 - 999999）
        int randomCode = (int)(Math.random() * 900000 + 100000);
        System.out.println("6位的随机整数：" + randomCode);
        /*
         将生成的验证码存入指定key的缓存，用于后续在相同手机号有效时间内再次访问时，直接从缓存中查询验证码，进行验证，
         而不是再次请求第三方接口获取新的验证码（设置了与短信验证码有效的过期时间）
         同时，设置key的值时，加入了存入缓存的当前系统时间，用于后续的接口防刷处理！
          */
        stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,
                randomCode+"_" + System.currentTimeMillis(), 10L,TimeUnit.MINUTES);

        thirdPartyFeignService.sendMsg(phone,randomCode+"");
        return R.ok();
    }

    // 表单注册
    @PostMapping("/regist")
    /*
    TODO：分布式下的session问题！
        重定向并携带数据，利用的是session原理，将数据存入session，跳到指定页面，然后取出其中的数据，该
        session里面的数据就会删除：redirectAttributes.addFlashAttribute("errs",errMap);

     */
    public String regist(@Valid UserRegistVo userRegistVo, BindingResult result,
                         /*Model model,*/RedirectAttributes redirectAttributes, HttpSession httpSession){
        // 将注册表单的信息封装成1个vo，并加入JSR32的校验功能，再次校验提交的数据格式
        System.out.println("auth服务获取到的userVo为：" + userRegistVo);
        List<FieldError> errs = result.getFieldErrors();
        if(result.hasErrors()){
            Map<String, String> errMap = errs.stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
//            model.addAttribute("errs",errMap);
            /*
             将请求请求转发给/register.html，由于仍是同一个请求对象，方式仍为POST方式，
             根据配置的路径视图映射都是GET方式访问，会出现：
             Request method 'POST' not supported，请求方式不支持的错误！
             解决方案：
              1）直接跳转到静态页面，携带model数据，但是一个页面提交表单，提交完后又回到当前页面，如果又要提交表单，就会
              出现表单重复提交的问题，所以还是采用方式2的 “ 请求重定向，并携带好之前的请求数据 ”！
              2）请求重定向，先回到浏览器（响应对象设置了要继续请求的地址信息），然后根据该地址信息由
              浏览器自行采用GET方式继续访问。但存在1个问题：新发起1次请求，会把之前请求对象中封装的数据
              弄丢失，若仍要采用请求重定向的方式，就使用RedirectAttributes类，同时封装好相应的数据
              */
//            System.out.println("发生异常的errMap为：\n" + errMap);
//            return "forward:/register.html";
//            return "register";// 直接跳转到静态页面
            // 封装请求携带的数据
            redirectAttributes.addFlashAttribute("errs",errMap);
            // 仅为了返回当前提交表单的页面，又得重新访问nginx进行代理、走网关作路由，性能太低了！还是推荐使用AJAX的异步回调来作
            return "redirect:http://auth.gulimall.com/register.html";
        }else{
            /*
             数据校验未发生格式错误，则可以继续实现注册的业务逻辑功能了
             先要判断缓存中是否存在指定手机号的验证码记录，没有直接返回错误的提示信息，
             如果有，还要判断传入的验证码和指定手机号的验证码记录是否相同，不相同也要
             返回错误的提示信息。只有指定手机号的缓存记录不为空且验证码相同，才能继续
             调用远程的member服务，对接注册接口，注册UserRegistVo的信息
              */
            String codeData = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + userRegistVo.getPhoneNumber());
            if(StringUtils.isEmpty(codeData)){
                Map<String,String> errMap = new HashMap<>();
                errMap.put("codeErr","验证码失效！");
                redirectAttributes.addFlashAttribute("errs",errMap);
                return "redirect:http://auth.gulimall.com/register.html";
            }else if(!codeData.split("_")[0].equals(userRegistVo.getCode())){
                Map<String,String> errMap = new HashMap<>();
                errMap.put("codeErr","验证码不正确！");
                redirectAttributes.addFlashAttribute("errs",errMap);
                return "redirect:http://auth.gulimall.com/register.html";
            }else {
                /*
                 如果该手机号有缓存记录且与传入的验证码相同，则先将缓存记录清除，防止重复利用验证码(令牌机制)
                 再来远程调用注册接口，进行注册
                  */
                stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX+userRegistVo.getPhoneNumber());
//                System.out.println("验证码校验成功，开始远程调用member的注册接口...");
                R r = memberFeignService.registerUser(userRegistVo);
//                System.out.println("远程调用结果状态为：" + r.getCode());
                if(r.getCode() == 0){
                    /*
                     注册成功，则跳转到login.html，如果直接通过return "login.html"访问登录页，则请求路径不会发生变化，
                     因为没有走请求的再次访问！只是单纯的通过Thymeleaf实现静态页面的切换、跳转！
                      */
                    return "redirect:http://auth.gulimall.com/login.html";
                }else{
                    // 注册失败，则携带失败原因，重新返回到register.html
                    Map<String,String> errMap = new HashMap<>();
                    errMap.put("msg",r.getData("msg",new TypeReference<String>(){}));
                    redirectAttributes.addFlashAttribute("errs",errMap);
                    return "redirect:http://auth.gulimall.com/register.html";
                }
            }
        }

    }

    // 页面登录
    @PostMapping("/login")
    public String login(UserLoginVo userLoginVo,RedirectAttributes redirectAttributes,HttpSession httpSession){
        R r = memberFeignService.login(userLoginVo);
        if(r.getCode() == 0){
            // 登录成功
            // 通过RedirectAttributes类，在请求重定向跳转时，携带调用远程member服务处理后的memberEntity类信息
            MemberUserInfoVo memberUserInfo = r.getData("memberUserInfo", new TypeReference<MemberUserInfoVo>() {
            });
            // 将登陆的用户信息，保存到Session会话中，并以Redis形式存储（注意：实体类需要实现序列化接口——序列化和反序列化）
            httpSession.setAttribute(AuthServerConstant.LOGIN_USER_KEY,memberUserInfo);
            return "redirect:http://gulimall.com/";
        }else{
            // 否则，携带验证的失败原因，跳转回登录页面
            Map<String,String> errMap = new HashMap<>();
            String msg = r.getData("msg",new TypeReference<String>(){});
            errMap.put("msg",msg);
            redirectAttributes.addFlashAttribute("errs",errMap);
            return "redirect:http://auth.gulimall.com/login.html";
        }
    }

    // 访问登录页，首先判断是否登录过，只有没登录过才访问到登录页，否则直接跳转到商城首页
    @GetMapping("/login.html")
    public String visitLogin(HttpSession httpSession){
        System.out.println("进入登录页面判断逻辑中...");
        MemberUserInfoVo userInfoVo = (MemberUserInfoVo) httpSession.getAttribute(AuthServerConstant.LOGIN_USER_KEY);
        if(userInfoVo != null){
            return "redirect:http://gulimall.com/";
        }else{
            return "login";
        }
    }




}
